{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Generate `nbgitpuller` links for your JupyterHub\n",
    "\n",
    "When users click an `nbgitpuller` link pointing to your JupyterHub,\n",
    "\n",
    "1. They are asked to log in to the JupyterHub if they have not already\n",
    "2. The git repository referred to in the nbgitpuller link is made up to date in their home directory (keeping local changes if there are merge conflicts)\n",
    "3. They are shown the specific notebook / directory referred to in the nbgitpuller link.\n",
    "\n",
    "This is a great way to distribute materials to students.\n",
    "\n",
    "# Generate `nbgitpuller` links for your JupyterHub\n",
    "\n",
    "## Sequence of events when users click an `nbgitpuller` link pointing to your JupyterHub,\n",
    "\n",
    "1. They are asked to log in to the JupyterHub if they have not already\n",
    "2. The git repository referred to in the nbgitpuller link is made up to date in their home directory (keeping local changes if there are merge conflicts)\n",
    "3. They are shown the specific notebook / directory referred to in the nbgitpuller link.\n",
    "\n",
    "This is a great way to distribute materials to students.\n",
    "\n",
    "## Canvas LMS: Assignment Links vs Custom Fields\n",
    "\n",
    "The Canvas LMS expects the assignment link to include URL encoded parameters since the request is sent to the External Tool as a POST request (in this case JupyterHub is the External Tool). However, all characters (even those considered safe) after the domain and `next=` part should be URL encoded, such as the `/`, `&`, and `=` characters.\n",
    "\n",
    "The `Custom Fields` text box in the App -> Settings section, on the other hand, does not expect all characters to be URL encoded. The `/` characters that are assigned as part of the query parameter values should be encoded, but not the `&` and `=` characters.\n",
    "\n",
    "## Usage\n",
    "\n",
    "- **Assignment Link**: creates a string value which represents an `Assignment` link by toggling the check box next to the `is_assignment_link` label. If unchecked, the tool will create a string to add to the Custom Field section.\n",
    "- **Jupyter Lab Link**: creates a string value which redirects the user to a `Jupyter Lab` workspace instead of the `Jupyter Classic` workspace.\n",
    "- **LTI Launches**: adds the route associated to the LTI 1.1 login handler. If disabled, it is assumed that the user is using the default authentication class bound to the root of the `domain_url` value.\n",
    "- **Default Values**: to avoid having to enter the same values in the widget's text fields on a repetitive basis, add the string values to the function's parameters. For example, the `branch` parameter defaults to `master`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "output_type": "display_data",
     "data": {
      "text/plain": "'https://my.hub.com/hub/lti/launch?next=%2Fuser-redirect%2Fgit-pull?repo%3D%26branch%3Dmaster%26urlpath%3Dlab%252Ftree%252F.%252F%253Fautodecode'"
     },
     "metadata": {}
    }
   ],
   "source": [
    "import os\n",
    "from ipywidgets import interact\n",
    "from urllib.parse import urlunparse, urlparse, urlencode, parse_qs, parse_qsl, quote\n",
    "from IPython.display import Markdown\n",
    "\n",
    "\n",
    "@interact\n",
    "def make_launch_link(is_assignment_link=True, is_jupyterlab=True, is_lti11=True, branch='master', hub_url='https://my.hub.com', repo_url='', urlpath=''):\n",
    "    \"\"\"\n",
    "    Generate a launch request which clones and merges source files from a git-based\n",
    "    repository.\n",
    "\n",
    "    Args:\n",
    "      is_assignment_link (bool): set to True to create a full assignment link, defaults to True.\n",
    "      is_jupyterlab (bool): set to True to launch Jupyter Lab workspaces, defaults to True.\n",
    "      is_lti11 (bool): set to True to initiate launch requests with the LTI 1.1 standard.\n",
    "      branch (str): git repo branch\n",
    "      hub_url (str): full hub url which needs to include scheme (http or https) and netloc (full domain).\n",
    "      repo_url (str): full git repo url which needs to include scheme (http or https), netloc (full domain) and path.\n",
    "      url_path (str): a path to redirect users to after the workspace has successfully spawned (started).\n",
    "\n",
    "    Returns:\n",
    "      An interactive IPython.display.Markdown object.\n",
    "    \"\"\"\n",
    "\n",
    "    # Parse the query to its constituent parts\n",
    "    domain_scheme, domain_netloc, domain_path, domain_params, domain_query_str, domain_fragment = urlparse(hub_url.strip())\n",
    "    \n",
    "    repo_scheme, repo_netloc, repo_path, repo_params, repo_query_str, repo_fragment = urlparse(repo_url.strip())\n",
    "    folder_from_repo_url_path = os.path.basename(os.path.normpath(repo_path))\n",
    "    \n",
    "    # Make sure the path doesn't contain multiple slashes\n",
    "    if not domain_path.endswith('/'):\n",
    "        domain_path += '/'\n",
    "    domain_path += 'user-redirect/git-pull'\n",
    "    \n",
    "    # With Canvas using LTI 11 Assignment launch requests all characters after the netloc are considered unsafe.\n",
    "    # When adding custom parameters within the App Settings -> Custom Fields section, only items after the \n",
    "    path_encoded = ''\n",
    "    if is_assignment_link:\n",
    "        path_encoded = quote(domain_path, safe='')\n",
    "    else:\n",
    "        path_encoded = quote(domain_path)\n",
    "\n",
    "    path_redirect_url = f'next={path_encoded}'\n",
    "    if is_lti11:\n",
    "        assignment_link_path = f'/hub/lti/launch?next={path_encoded}'\n",
    "    else:\n",
    "        assignment_link_path = f'/hub?next={path_encoded}'\n",
    "    \n",
    "    # Create a tuple of query params from original domain link\n",
    "    query_params_from_hub_url = parse_qsl(domain_query_str, keep_blank_values=True)\n",
    "    \n",
    "    # Set path based on whether or not the user would like to spawn JupyterLab or Jupyter Classic\n",
    "    urlpath_workspace = ''\n",
    "    if is_jupyterlab:\n",
    "        urlpath_workspace = f'lab/tree/{folder_from_repo_url_path}/{urlpath}?autodecode'\n",
    "    else:\n",
    "        urlpath_workspace = f'tree/{folder_from_repo_url_path}/{urlpath}'\n",
    "    \n",
    "    # Create a tuple of query params for git functionality. Check whether or not we want to launch with\n",
    "    # jupyterlab to add additional items to the path.\n",
    "    query_params_for_git = [('repo', repo_url), ('branch', branch), ('urlpath', urlpath_workspace)]\n",
    "    \n",
    "    # Merge query params into one list of tuples\n",
    "    query_params_all = query_params_from_hub_url + query_params_for_git\n",
    "    \n",
    "    # First build urlencoded query params where the &, =, and / are considered safe. Then, percent encode\n",
    "    # all characters.\n",
    "    encoded_query_params = urlencode(query_params_all)\n",
    "    encoded_query_params_without_safe_chars = quote(urlencode(query_params_all), safe='')\n",
    "    \n",
    "    assignment_link_url = urlunparse((domain_scheme, domain_netloc, assignment_link_path, domain_params, encoded_query_params_without_safe_chars, domain_fragment))\n",
    "    path_url = urlunparse(('', '', path_redirect_url, domain_params, encoded_query_params, domain_fragment))\n",
    "    \n",
    "    if is_assignment_link:\n",
    "        return assignment_link_url\n",
    "    return path_url"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.1-final"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
